什么是事件循环(event loop)?

尽管js是单线程的,事件循环机制,通过在合适的时候把操作交给系统内核,从而允许node执行非阻塞的io操作
当操作完成时,内核告知node.js,合适的回调函数会被加入轮询队列,最终被执行。
Node.js启动的时候,初始化event loop,处理提供的脚本,脚本中可能调用异步API,调度timers,或者调用process.nextTick(),然后处理event loop

下图是简化的事件循环操作顺序图overview

     ┌───────────────────────┐
┌─>│        timers         │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     I/O callbacks     │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     idle, prepare     │
│  └──────────┬────────────┘      ┌───────────────┐
│  ┌──────────┴────────────┐      │   incoming:   │
│  │         poll          │<─────┤  connections, │
│  └──────────┬────────────┘      │   data, etc.  │
│  ┌──────────┴────────────┐      └───────────────┘
│  │        check          │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
└──┤    close callbacks    │
      └───────────────────────┘

图中每个box就是一个phase,每个phase有一个先进先出的回调函数的队列,
event loop进入了一个phase,就会执行phase中所有的操作,然后执行回调函数,直到队列耗尽了,或者回调函数执行数量到达最大数,接下来就去下一个phase

因为任何一个操作都可能调度更多的操作,而且poll phase中新的事件由内核排队,所以正在轮询的事件在被处理的时候,poll事件们可能会排队。
结果:长时间的运行回调函数允许poll phase运行事件比timer的阈值更长。

phase overview 阶段概况

  • timers:执行由setTimeout() and setInterval()调度的回调函数

  • I/O callbacks:执行所有的回调函数,除了 close callbacks(由timers,setImmediate()调度)

  • idle, prepare:内部使用

  • poll:获取新的io事件,当合适的时候,node会阻塞在这里

  • check: setImmediate()回调函数会在这里调用

  • close callbacks: e.g. socket.on('close', ...)

每次运行event loop,node检查是否有对任何异步io或者timers的等待,没有就关闭

Phases in Detail(各阶段细述)

timers

timers指定阈值(threshold)之后,会执行回调函数,但threshold不是执行回调函数的确切时间(只是最短时间)。
timers回调函数一旦可以执行了就会被执行。然而操作系统的调度或者其他的回调函数可能推迟它的执行。
由poll phase来控制什么时候timers被执行

var fs = require('fs');
function someAsyncOperation (callback) {
  // Assume this takes 95ms to complete
  fs.readFile('/path/to/file', callback);
}
var timeoutScheduled = Date.now();
setTimeout(function () {
  var delay = Date.now() - timeoutScheduled;
  console.log(delay + "ms have passed since I was scheduled");
}, 100);
// do someAsyncOperation which takes 95 ms to complete
someAsyncOperation(function () {
  var startCallback = Date.now();
  // do something that will take 10ms...
  while (Date.now() - startCallback < 10) {
    ; // do nothing
  }
});

一开始timer被调度,里面的回调函数执行log。
然后事件循环进入poll phase,此时队列是空的(因为fs.readFile()没有完成),所以就会等着,直到最早的timer的阈值(100)到时间,等了95 ms(还没到,毕竟定的是100),fs.readFile() 这个时候完成了,所以它的回调函数就回被加poll的队列并且被执行(执行10s),当回调函数完成了,队列又空了,所以,event loop将会看到timer的阈值(100)已经到了,
然后回到timers这个phase去执行timers的回调函数,也就是,打印出105秒

为了防止poll phase 独占耗尽 event loop,libuv 也有一个最大值(基于系统),会在超过最大值之前停止轮询更多的事件。

I/O callbacks

为系统操作(比如tcp错误类型)执行回调函数
当tcp socket尝试连接时接收到ECONNREFUSED,类unix系统将会想报道错误,要会在这个phase排队执行。

poll

poll phase有两个功能

  • 为到了时间的timers执行脚本,然后

  • 处理poll队列的事件

当event loop 进入poll phase且没有timers被调度,下面的事情会发生

  • poll不空,

    • 通过回调函数队列迭代的执行

  • poll栈是空的

    • 如果脚本已经被setImmediate()调度,事件循环将会终止poll phase,到check phase去执行那些被调度的脚本

    • 等着回调函数被加进队列,然后立马执行它
      一旦poll空了,event loop将回检查timers有没有thresholds到了,有的话,wrap back to the timers phase,然后执行timers的回调函数

check

特别的 timer

close callbacks

setImmediate and setTimeout()

  • 在poll完成以后执行

  • 在最小事件之后执行

执行顺序:
依赖于调用的上下文

  • 如果都在main module ,事件会被进程的性能限制(被其他应用影响)

    • not within an I/O cycle:不确定的

    • within an I/O cycle:immediate总是先(更好)

// timeout_vs_immediate.js
setTimeout(function timeout () {
  console.log('timeout');
},0);

setImmediate(function immediate () {
  console.log('immediate');
});
// timeout_vs_immediate.js
var fs = require('fs')

fs.readFile(__filename, () => {
  setTimeout(() => {
    console.log('timeout')
  }, 0)
  setImmediate(() => {
    console.log('immediate')
  })
})

The Node.js Event Loop, Timers

参考:


小帆
19 声望2 粉丝